Skip to content

fix(macos): recover capture after the display sleeps - #5510

Open
harrison001 wants to merge 1 commit into
LizardByte:masterfrom
harrison001:fix-macos-display-sleep
Open

fix(macos): recover capture after the display sleeps#5510
harrison001 wants to merge 1 commit into
LizardByte:masterfrom
harrison001:fix-macos-display-sleep

Conversation

@harrison001

@harrison001 harrison001 commented Aug 15, 2026

Copy link
Copy Markdown

Description

Display sleep can stall capture on macOS, and the stream never recovers.

The semaphore in capture() isn't signaled for every frame. av_video.m only signals it when the frame callback returns false, which normally happens when the session ends. If the display goes to sleep, buffers stop arriving and the capture thread can sit there forever.

Use a timed wait instead and check CGDisplayIsAsleep. If the display goes to sleep and later wakes up, stop the current capture and return capture_e::reinit. video.cpp treats the other return values as the end of the session, so this needs to be reinit.

A frame timeout doesn't work here because AVCaptureScreenInput is change-driven. An idle desktop may not produce frames either, so lack of frames alone doesn't tell us capture is stuck. Watching the display sleep/wake state avoids that ambiguity.

dummy_img() had the same unbounded wait. There the semaphore really does mean one frame arrived, but encoder probing would hang forever if the display was already asleep at startup, so that wait is bounded now too.

Also use stopCapture: rather than calling stopRunning() directly. stopRunning() on its own leaves the output registered with the session and in AVVideo's map tables. dealloc then releases those map tables before stopping the session, so the output gets released while the session still holds it.

No unit test for this one. It needs a real display going to sleep and waking up, which I can't reproduce in a test. Verified on my hardware instead: macOS 26.6.1, M1 Max, built-in display, streaming to Moonlight iOS. Capture rebuilt 86 ms and 62 ms after wake across two sleep/wake cycles, fresh IDR keyframe 264 ms after wake, one client session survived both cycles, no crash reports. I don't have a second display, so the external display path is untested.

Screenshot

Issues Fixed or Closed

Roadmap Issues

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

See our AI usage policy.

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

Capture waited on the session semaphore with DISPATCH_TIME_FOREVER, but that
semaphore is only signalled when the frame callback returns false — that is,
when capture is deliberately stopped. A sleeping display stops
AVCaptureSession delivering sample buffers and the session does not resume
when the display wakes, so the capture thread parked for the lifetime of the
process. Since capture() could also only ever return capture_e::ok, and
video.cpp rebuilds the display only on capture_e::reinit, nothing could bring
the stream back and the user had to restart Sunshine.

Poll the semaphore instead and watch the display while waiting. A display
that slept and then woke returns capture_e::reinit so the caller rebuilds it.
Waiting for the wake transition rather than for a frame-arrival timeout keeps
a static screen from being mistaken for a stalled one.

Abandoning a capture needs the same teardown the frame callback performs when
it returns false, so AVVideo grows a stopCapture: for it. Merely stopping the
session leaves the output in the capture session and in the map tables, and
-[AVVideo dealloc] releases those map tables before it stops the session, so
the output is released while the session still holds it and the process dies
in objc_msgSend during the next reinitialization.

dummy_img() had the same unbounded wait, where the semaphore genuinely does
mean "one frame arrived". Encoder probing hung there indefinitely when the
display was asleep, so bound it and report failure instead.

Tested on macOS 26.6.1 (Apple M1 Max): with a stream running, a hot corner set
to Put Display to Sleep, and the display then woken, capture is rebuilt in
under 100 ms and a fresh IDR frame follows within 300 ms. Two sleep/wake
cycles in a row were survived by a single uninterrupted client session.
@ReenigneArcher

Copy link
Copy Markdown
Member

Thank you for the PR submission, but it looks like you used AI to create this PR.

Please read and follow our Contributing guidelines and specifically our AI Usage policy.

Additionally, please update the PR to use the correct template. You can find it at https://github.com/LizardByte/.github/blob/master/.github/pull_request_template.md?plain=1

@ReenigneArcher ReenigneArcher added the ai PR has signs of heavy ai usage (either indicated by user or assumed) label Aug 15, 2026
@sonarqubecloud

Copy link
Copy Markdown

@harrison001

Copy link
Copy Markdown
Author

I am sorry, I skipped the template. I have fixed and marked AI usage as Heavy.

Worth adding that the first version of this patch was wrong. It detected the wake correctly and logged the right line, then killed the process ten seconds later . The reason: dealloc frees the map tables before stopping the session. That's what stopCapture: is for. I found it by running it, not by reading it.

Please review and let me know if further changes are needed. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai PR has signs of heavy ai usage (either indicated by user or assumed)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS: display sleep permanently stalls capture and the stream cannot recover

2 participants